package com.icesoft.base.manager.security.login.password;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

@Deprecated
class SimpleHash {

	private static final int DEFAULT_ITERATIONS = 1;

	/**
	 * The {@link MessageDigest MessageDigest} algorithm name to
	 * use when performing the hash.
	 */
	private final String algorithmName;

	/**
	 * The hashed data
	 */
	private byte[] bytes;

	/**
	 * Number of hash iterations to perform. Defaults to 1 in the constructor.
	 */
	private int iterations;

	/**
	 * Cached value of the {@link #toHex() toHex()} call so multiple calls won't
	 * incur repeated overhead.
	 */
	private transient String hexEncoded = null;

	public SimpleHash(String algorithmName) {
		this.algorithmName = algorithmName;
		this.iterations = DEFAULT_ITERATIONS;
	}

	public SimpleHash(String algorithmName, String source) {
		this(algorithmName, source, null, DEFAULT_ITERATIONS);
	}

	/**
	 * MD5,SHA-1,SHA-256,SHA-512
	 */
	public SimpleHash(String algorithmName, String source, String salt) {
		this(algorithmName, source, salt, DEFAULT_ITERATIONS);
	}

	public SimpleHash(String algorithmName, String source, String salt, int hashIterations) {
		this.algorithmName = algorithmName;
		this.iterations = Math.max(DEFAULT_ITERATIONS, hashIterations);
		byte[] saltBytes = null;
		if (salt != null) {
			saltBytes = salt.getBytes();
		}
		byte[] sourceBytes = source.getBytes();
		hash(sourceBytes, saltBytes, hashIterations);
	}

	public String getAlgorithmName() {
		return this.algorithmName;
	}

	public int getIterations() {
		return this.iterations;
	}

	public byte[] getBytes() {
		return this.bytes;
	}

	/**
	 * Sets the raw bytes stored by this hash instance.
	 * <p/>
	 * The bytes are kept in raw form - they will not be hashed/changed. This is
	 * primarily a utility method for constructing a Hash instance when the
	 * hashed value is already known.
	 *
	 * @param alreadyHashedBytes
	 *            the raw already-hashed bytes to store in this instance.
	 */
	public void setBytes(byte[] alreadyHashedBytes) {
		this.bytes = alreadyHashedBytes;
		this.hexEncoded = null;
	}

	/**
	 * Sets the iterations used to previously compute AN ALREADY GENERATED HASH.
	 * <p/>
	 * This is provided <em>ONLY</em> to reconstitute an already-created Hash
	 * instance. It should ONLY ever be invoked when re-constructing a hash
	 * instance from an already-hashed value.
	 *
	 * @param iterations
	 *            the number of hash iterations used to previously create the
	 *            hash/digest.
	 * @since 1.2
	 */
	public void setIterations(int iterations) {
		this.iterations = Math.max(DEFAULT_ITERATIONS, iterations);
	}

	/**
	 * Returns the JDK MessageDigest instance to use for executing the hash.
	 *
	 * @param algorithmName
	 *            the algorithm to use for the hash, provided by subclasses.
	 * @return the MessageDigest object for the specified {@code algorithm}.
	 * @throws UnknownAlgorithmException
	 *             if the specified algorithm name is not available.
	 */
	protected MessageDigest getDigest(String algorithmName) {
		try {
			return MessageDigest.getInstance(algorithmName);
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		return null;
	}

	protected byte[] hash(byte[] bytes, byte[] salt, int hashIterations) {
		MessageDigest digest = getDigest(getAlgorithmName());
		if (salt != null) {
			digest.reset();
			digest.update(salt);
		}
		byte[] hashed = digest.digest(bytes);
		int iterations = hashIterations - DEFAULT_ITERATIONS; // already hashed
																// once above
		// iterate remaining number:
		for (int i = 0; i < iterations; i++) {
			digest.reset();
			hashed = digest.digest(hashed);
		}
		setBytes(hashed);
		return hashed;
	}

	/**
	 * Returns a hex-encoded string of the underlying {@link #getBytes byte
	 * array}.
	 * <p/>
	 * This implementation caches the resulting hex string so multiple calls to
	 * this method remain efficient. However, calling {@link #setBytes setBytes}
	 * will null the cached value, forcing it to be recalculated the next time
	 * this method is called.
	 *
	 * @return a hex-encoded string of the underlying {@link #getBytes byte
	 *         array}.
	 */
	public String toHex() {
		if (this.hexEncoded == null) {
			this.hexEncoded = encodeToString(getBytes());
		}
		return this.hexEncoded;
	}

	/**
	 * Simple implementation that merely returns {@link #toHex() toHex()}.
	 *
	 * @return the {@link #toHex() toHex()} value.
	 */
	public String toString() {
		return toHex();
	}

	/**
	 * Simply returns toHex().hashCode();
	 *
	 * @return toHex().hashCode()
	 */
	public int hashCode() {
		if (this.bytes == null || this.bytes.length == 0) {
			return 0;
		}
		return Arrays.hashCode(this.bytes);
	}

	/**
	 * Used to build output as Hex
	 */
	private static final char[] DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e',
			'f' };

	/**
	 * Encodes the specifed byte array to a character array and then returns
	 * that character array as a String.
	 *
	 * @param bytes
	 *            the byte array to Hex-encode.
	 * @return A String representation of the resultant hex-encoded char array.
	 */
	public static String encodeToString(byte[] bytes) {
		char[] encodedChars = encode(bytes);
		return new String(encodedChars);
	}

	/**
	 * Converts an array of bytes into an array of characters representing the
	 * hexidecimal values of each byte in order. The returned array will be
	 * double the length of the passed array, as it takes two characters to
	 * represent any given byte.
	 *
	 * @param data
	 *            byte[] to convert to Hex characters
	 * @return A char[] containing hexidecimal characters
	 */
	public static char[] encode(byte[] data) {

		int l = data.length;

		char[] out = new char[l << 1];

		// two characters form the hex value.
		for (int i = 0, j = 0; i < l; i++) {
			out[j++] = DIGITS[(0xF0 & data[i]) >>> 4];
			out[j++] = DIGITS[0x0F & data[i]];
		}

		return out;
	}

	/**
	 * Converts the specified Hex-encoded String into a raw byte array. This is
	 * a convenience method that merely delegates to {@link #decode(char[])}
	 * using the argument's hex.toCharArray() value.
	 *
	 * @param hex
	 *            a Hex-encoded String.
	 * @return A byte array containing binary data decoded from the supplied
	 *         String's char array.
	 */
	public static byte[] decode(String hex) {
		return decode(hex.toCharArray());
	}

	/**
	 * Converts an array of characters representing hexidecimal values into an
	 * array of bytes of those same values. The returned array will be half the
	 * length of the passed array, as it takes two characters to represent any
	 * given byte. An exception is thrown if the passed char array has an odd
	 * number of elements.
	 *
	 * @param data
	 *            An array of characters containing hexidecimal digits
	 * @return A byte array containing binary data decoded from the supplied
	 *         char array.
	 * @throws IllegalArgumentException
	 *             if an odd number or illegal of characters is supplied
	 */
	public static byte[] decode(char[] data) throws IllegalArgumentException {

		int len = data.length;

		if ((len & 0x01) != 0) {
			throw new IllegalArgumentException("Odd number of characters.");
		}

		byte[] out = new byte[len >> 1];

		// two characters form the hex value.
		for (int i = 0, j = 0; j < len; i++) {
			int f = toDigit(data[j], j) << 4;
			j++;
			f = f | toDigit(data[j], j);
			j++;
			out[i] = (byte) (f & 0xFF);
		}

		return out;
	}

	/**
	 * Converts a hexadecimal character to an integer.
	 *
	 * @param ch
	 *            A character to convert to an integer digit
	 * @param index
	 *            The index of the character in the source
	 * @return An integer
	 * @throws IllegalArgumentException
	 *             if ch is an illegal hex character
	 */
	protected static int toDigit(char ch, int index) throws IllegalArgumentException {
		int digit = Character.digit(ch, 16);
		if (digit == -1) {
			throw new IllegalArgumentException("Illegal hexadecimal charcter " + ch + " at index " + index);
		}
		return digit;
	}

}
